- Hook Development for Claude Code Plugins
- Overview
- Hooks are event-driven automation scripts that execute in response to Claude Code events. Use hooks to validate operations, enforce policies, add context, and integrate external tools into workflows.
- Key capabilities:
- Validate tool calls before execution (PreToolUse)
- React to tool results (PostToolUse)
- Enforce completion standards (Stop, SubagentStop)
- Load project context (SessionStart)
- Automate workflows across the development lifecycle
- Hook Types
- Prompt-Based Hooks (Recommended)
- Use LLM-driven decision making for context-aware validation:
- {
- "type"
- :
- "prompt"
- ,
- "prompt"
- :
- "Evaluate if this tool use is appropriate: $TOOL_INPUT"
- ,
- "timeout"
- :
- 30
- }
- Supported events:
- Stop, SubagentStop, UserPromptSubmit, PreToolUse
- Benefits:
- Context-aware decisions based on natural language reasoning
- Flexible evaluation logic without bash scripting
- Better edge case handling
- Easier to maintain and extend
- Command Hooks
- Execute bash commands for deterministic checks:
- {
- "type"
- :
- "command"
- ,
- "command"
- :
- "bash ${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh"
- ,
- "timeout"
- :
- 60
- }
- Use for:
- Fast deterministic validations
- File system operations
- External tool integrations
- Performance-critical checks
- Hook Configuration Formats
- Plugin hooks.json Format
- For plugin hooks
- in
- hooks/hooks.json
- , use wrapper format:
- {
- "description"
- :
- "Brief explanation of hooks (optional)"
- ,
- "hooks"
- :
- {
- "PreToolUse"
- :
- [
- ...
- ]
- ,
- "Stop"
- :
- [
- ...
- ]
- ,
- "SessionStart"
- :
- [
- ...
- ]
- }
- }
- Key points:
- description
- field is optional
- hooks
- field is required wrapper containing actual hook events
- This is the
- plugin-specific format
- Example:
- {
- "description"
- :
- "Validation hooks for code quality"
- ,
- "hooks"
- :
- {
- "PreToolUse"
- :
- [
- {
- "matcher"
- :
- "Write"
- ,
- "hooks"
- :
- [
- {
- "type"
- :
- "command"
- ,
- "command"
- :
- "${CLAUDE_PLUGIN_ROOT}/hooks/validate.sh"
- }
- ]
- }
- ]
- }
- }
- Settings Format (Direct)
- For user settings
- in
- .claude/settings.json
- , use direct format:
- {
- "PreToolUse"
- :
- [
- ...
- ]
- ,
- "Stop"
- :
- [
- ...
- ]
- ,
- "SessionStart"
- :
- [
- ...
- ]
- }
- Key points:
- No wrapper - events directly at top level
- No description field
- This is the
- settings format
- Important:
- The examples below show the hook event structure that goes inside either format. For plugin hooks.json, wrap these in
- {"hooks": {...}}
- .
- Hook Events
- PreToolUse
- Execute before any tool runs. Use to approve, deny, or modify tool calls.
- Example (prompt-based):
- {
- "PreToolUse"
- :
- [
- {
- "matcher"
- :
- "Write|Edit"
- ,
- "hooks"
- :
- [
- {
- "type"
- :
- "prompt"
- ,
- "prompt"
- :
- "Validate file write safety. Check: system paths, credentials, path traversal, sensitive content. Return 'approve' or 'deny'."
- }
- ]
- }
- ]
- }
- Output for PreToolUse:
- {
- "hookSpecificOutput"
- :
- {
- "permissionDecision"
- :
- "allow|deny|ask"
- ,
- "updatedInput"
- :
- {
- "field"
- :
- "modified_value"
- }
- }
- ,
- "systemMessage"
- :
- "Explanation for Claude"
- }
- PostToolUse
- Execute after tool completes. Use to react to results, provide feedback, or log.
- Example:
- {
- "PostToolUse"
- :
- [
- {
- "matcher"
- :
- "Edit"
- ,
- "hooks"
- :
- [
- {
- "type"
- :
- "prompt"
- ,
- "prompt"
- :
- "Analyze edit result for potential issues: syntax errors, security vulnerabilities, breaking changes. Provide feedback."
- }
- ]
- }
- ]
- }
- Output behavior:
- Exit 0: stdout shown in transcript
- Exit 2: stderr fed back to Claude
- systemMessage included in context
- Stop
- Execute when main agent considers stopping. Use to validate completeness.
- Example:
- {
- "Stop"
- :
- [
- {
- "matcher"
- :
- "*"
- ,
- "hooks"
- :
- [
- {
- "type"
- :
- "prompt"
- ,
- "prompt"
- :
- "Verify task completion: tests run, build succeeded, questions answered. Return 'approve' to stop or 'block' with reason to continue."
- }
- ]
- }
- ]
- }
- Decision output:
- {
- "decision"
- :
- "approve|block"
- ,
- "reason"
- :
- "Explanation"
- ,
- "systemMessage"
- :
- "Additional context"
- }
- SubagentStop
- Execute when subagent considers stopping. Use to ensure subagent completed its task.
- Similar to Stop hook, but for subagents.
- UserPromptSubmit
- Execute when user submits a prompt. Use to add context, validate, or block prompts.
- Example:
- {
- "UserPromptSubmit"
- :
- [
- {
- "matcher"
- :
- "*"
- ,
- "hooks"
- :
- [
- {
- "type"
- :
- "prompt"
- ,
- "prompt"
- :
- "Check if prompt requires security guidance. If discussing auth, permissions, or API security, return relevant warnings."
- }
- ]
- }
- ]
- }
- SessionStart
- Execute when Claude Code session begins. Use to load context and set environment.
- Example:
- {
- "SessionStart"
- :
- [
- {
- "matcher"
- :
- "*"
- ,
- "hooks"
- :
- [
- {
- "type"
- :
- "command"
- ,
- "command"
- :
- "bash ${CLAUDE_PLUGIN_ROOT}/scripts/load-context.sh"
- }
- ]
- }
- ]
- }
- Special capability:
- Persist environment variables using
- $CLAUDE_ENV_FILE
- :
- echo
- "export PROJECT_TYPE=nodejs"
- >>
- "
- $CLAUDE_ENV_FILE
- "
- See
- examples/load-context.sh
- for complete example.
- SessionEnd
- Execute when session ends. Use for cleanup, logging, and state preservation.
- PreCompact
- Execute before context compaction. Use to add critical information to preserve.
- Notification
- Execute when Claude sends notifications. Use to react to user notifications.
- Hook Output Format
- Standard Output (All Hooks)
- {
- "continue"
- :
- true
- ,
- "suppressOutput"
- :
- false
- ,
- "systemMessage"
- :
- "Message for Claude"
- }
- continue
-
- If false, halt processing (default true)
- suppressOutput
-
- Hide output from transcript (default false)
- systemMessage
- Message shown to Claude Exit Codes 0 - Success (stdout shown in transcript) 2 - Blocking error (stderr fed back to Claude) Other - Non-blocking error Hook Input Format All hooks receive JSON via stdin with common fields: { "session_id" : "abc123" , "transcript_path" : "/path/to/transcript.txt" , "cwd" : "/current/working/dir" , "permission_mode" : "ask|allow" , "hook_event_name" : "PreToolUse" } Event-specific fields: PreToolUse/PostToolUse: tool_name , tool_input , tool_result UserPromptSubmit: user_prompt Stop/SubagentStop: reason Access fields in prompts using $TOOL_INPUT , $TOOL_RESULT , $USER_PROMPT , etc. Environment Variables Available in all command hooks: $CLAUDE_PROJECT_DIR - Project root path $CLAUDE_PLUGIN_ROOT - Plugin directory (use for portable paths) $CLAUDE_ENV_FILE - SessionStart only: persist env vars here $CLAUDE_CODE_REMOTE - Set if running in remote context Always use ${CLAUDE_PLUGIN_ROOT} in hook commands for portability: { "type" : "command" , "command" : "bash ${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh" } Plugin Hook Configuration In plugins, define hooks in hooks/hooks.json : { "PreToolUse" : [ { "matcher" : "Write|Edit" , "hooks" : [ { "type" : "prompt" , "prompt" : "Validate file write safety" } ] } ] , "Stop" : [ { "matcher" : "" , "hooks" : [ { "type" : "prompt" , "prompt" : "Verify task completion" } ] } ] , "SessionStart" : [ { "matcher" : "" , "hooks" : [ { "type" : "command" , "command" : "bash ${CLAUDE_PLUGIN_ROOT}/scripts/load-context.sh" , "timeout" : 10 } ] } ] } Plugin hooks merge with user's hooks and run in parallel. Matchers Tool Name Matching Exact match: "matcher" : "Write" Multiple tools: "matcher" : "Read|Write|Edit" Wildcard (all tools): "matcher" : "" Regex patterns: "matcher" : "mcp__.__delete." // All MCP delete tools Note: Matchers are case-sensitive. Common Patterns // All MCP tools "matcher" : "mcp__." // Specific plugin's MCP tools "matcher" : "mcp__plugin_asana_.*" // All file operations "matcher" : "Read|Write|Edit" // Bash commands only "matcher" : "Bash" Security Best Practices Input Validation Always validate inputs in command hooks:
!/bin/bash
set -euo pipefail input = $( cat ) tool_name = $( echo " $input " | jq -r '.tool_name' )
Validate tool name format
if [ [ ! " $tool_name " =~ ^ [ a-zA-Z0-9_ ] +$ ] ] ; then echo '{"decision": "deny", "reason": "Invalid tool name"}'
&2 exit 2 fi Path Safety Check for path traversal and sensitive files: file_path = $( echo " $input " | jq -r '.tool_input.file_path' )
Deny path traversal
if [ [ " $file_path " == * ".." * ] ] ; then echo '{"decision": "deny", "reason": "Path traversal detected"}'
&2 exit 2 fi
Deny sensitive files
if [ [ " $file_path " == * ".env" * ] ] ; then echo '{"decision": "deny", "reason": "Sensitive file"}'
&2 exit 2 fi See examples/validate-write.sh and examples/validate-bash.sh for complete examples. Quote All Variables
GOOD: Quoted
echo " $file_path " cd " $CLAUDE_PROJECT_DIR "
BAD: Unquoted (injection risk)
echo $file_path cd $CLAUDE_PROJECT_DIR Set Appropriate Timeouts { "type" : "command" , "command" : "bash script.sh" , "timeout" : 10 } Defaults: Command hooks (60s), Prompt hooks (30s) Performance Considerations Parallel Execution All matching hooks run in parallel : { "PreToolUse" : [ { "matcher" : "Write" , "hooks" : [ { "type" : "command" , "command" : "check1.sh" } , // Parallel { "type" : "command" , "command" : "check2.sh" } , // Parallel { "type" : "prompt" , "prompt" : "Validate..." } // Parallel ] } ] } Design implications: Hooks don't see each other's output Non-deterministic ordering Design for independence Optimization Use command hooks for quick deterministic checks Use prompt hooks for complex reasoning Cache validation results in temp files Minimize I/O in hot paths Temporarily Active Hooks Create hooks that activate conditionally by checking for a flag file or configuration: Pattern: Flag file activation
!/bin/bash
Only active when flag file exists
FLAG_FILE
" $CLAUDE_PROJECT_DIR /.enable-strict-validation" if [ ! -f " $FLAG_FILE " ] ; then
Flag not present, skip validation
exit 0 fi
Flag present, run validation
input
$( cat )
... validation logic ...
Pattern: Configuration-based activation
!/bin/bash
Check configuration for activation
CONFIG_FILE
" $CLAUDE_PROJECT_DIR /.claude/plugin-config.json" if [ -f " $CONFIG_FILE " ] ; then enabled = $( jq -r '.strictMode // false' " $CONFIG_FILE " ) if [ " $enabled " != "true" ] ; then exit 0
Not enabled, skip
fi fi
Enabled, run hook logic
input
$( cat )
... hook logic ...
- Use cases:
- Enable strict validation only when needed
- Temporary debugging hooks
- Project-specific hook behavior
- Feature flags for hooks
- Best practice:
- Document activation mechanism in plugin README so users know how to enable/disable temporary hooks.
- Hook Lifecycle and Limitations
- Hooks Load at Session Start
- Important:
- Hooks are loaded when Claude Code session starts. Changes to hook configuration require restarting Claude Code.
- Cannot hot-swap hooks:
- Editing
- hooks/hooks.json
- won't affect current session
- Adding new hook scripts won't be recognized
- Changing hook commands/prompts won't update
- Must restart Claude Code: exit and run
- claude
- again
- To test hook changes:
- Edit hook configuration or scripts
- Exit Claude Code session
- Restart:
- claude
- or
- cc
- New hook configuration loads
- Test hooks with
- claude --debug
- Hook Validation at Startup
- Hooks are validated when Claude Code starts:
- Invalid JSON in hooks.json causes loading failure
- Missing scripts cause warnings
- Syntax errors reported in debug mode
- Use
- /hooks
- command to review loaded hooks in current session.
- Debugging Hooks
- Enable Debug Mode
- claude
- --debug
- Look for hook registration, execution logs, input/output JSON, and timing information.
- Test Hook Scripts
- Test command hooks directly:
- echo
- '{"tool_name": "Write", "tool_input": {"file_path": "/test"}}'
- |
- \
- bash
- ${CLAUDE_PLUGIN_ROOT}
- /scripts/validate.sh
- echo
- "Exit code:
- $?
- "
- Validate JSON Output
- Ensure hooks output valid JSON:
- output
- =
- $(
- ./your-hook.sh
- <
- test-input.json
- )
- echo
- "
- $output
- "
- |
- jq
- .
- Quick Reference
- Hook Events Summary
- Event
- When
- Use For
- PreToolUse
- Before tool
- Validation, modification
- PostToolUse
- After tool
- Feedback, logging
- UserPromptSubmit
- User input
- Context, validation
- Stop
- Agent stopping
- Completeness check
- SubagentStop
- Subagent done
- Task validation
- SessionStart
- Session begins
- Context loading
- SessionEnd
- Session ends
- Cleanup, logging
- PreCompact
- Before compact
- Preserve context
- Notification
- User notified
- Logging, reactions
- Best Practices
- DO:
- ✅ Use prompt-based hooks for complex logic
- ✅ Use ${CLAUDE_PLUGIN_ROOT} for portability
- ✅ Validate all inputs in command hooks
- ✅ Quote all bash variables
- ✅ Set appropriate timeouts
- ✅ Return structured JSON output
- ✅ Test hooks thoroughly
- DON'T:
- ❌ Use hardcoded paths
- ❌ Trust user input without validation
- ❌ Create long-running hooks
- ❌ Rely on hook execution order
- ❌ Modify global state unpredictably
- ❌ Log sensitive information
- Additional Resources
- Reference Files
- For detailed patterns and advanced techniques, consult:
- references/patterns.md
- - Common hook patterns (8+ proven patterns)
- references/migration.md
- - Migrating from basic to advanced hooks
- references/advanced.md
- - Advanced use cases and techniques
- Example Hook Scripts
- Working examples in
- examples/
- :
- validate-write.sh
- - File write validation example
- validate-bash.sh
- - Bash command validation example
- load-context.sh
- - SessionStart context loading example
- Utility Scripts
- Development tools in
- scripts/
- :
- validate-hook-schema.sh
- - Validate hooks.json structure and syntax
- test-hook.sh
- - Test hooks with sample input before deployment
- hook-linter.sh
- - Check hook scripts for common issues and best practices
- External Resources
- Official Docs
- :
- https://docs.claude.com/en/docs/claude-code/hooks
- Examples
-
- See security-guidance plugin in marketplace
- Testing
-
- Use
- claude --debug
- for detailed logs
- Validation
- Use jq to validate hook JSON output Implementation Workflow To implement hooks in a plugin: Identify events to hook into (PreToolUse, Stop, SessionStart, etc.) Decide between prompt-based (flexible) or command (deterministic) hooks Write hook configuration in hooks/hooks.json For command hooks, create hook scripts Use ${CLAUDE_PLUGIN_ROOT} for all file references Validate configuration with scripts/validate-hook-schema.sh hooks/hooks.json Test hooks with scripts/test-hook.sh before deployment Test in Claude Code with claude --debug Document hooks in plugin README Focus on prompt-based hooks for most use cases. Reserve command hooks for performance-critical or deterministic checks.